package ru.xeden.web.html.dom;

import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;

import ru.xeden.core.common.Utils;
import ru.xeden.web.html.RenderContext;


public abstract class Element
    extends Node
{
    private INode firstChild;
    private INode lastChild;
    private Attribute firstAttribute;
    private static final String CLASS_ATTRIBUTE = "class";

    /**
     * Adds a namespaced attribute to the element, but only if the attribute name does not already exist.
     *
     * @param namespace the namespace to contain the attribute, or null
     * @param name      the name of the attribute to add
     * @param value     the value for the attribute. A value of null is allowed, and no attribute will be added to the
     *                  element.
     */
    public Element attribute(String name, String value)
    {
        assert !Utils.isEmpty(name);
        updateAttribute(name, value, false);
        return this;
    }

    private void updateAttribute(String name, String value, boolean force)
    {
        if (!force && value == null)
            return;
        Attribute prior = null;
        Attribute cursor = firstAttribute;
        while (cursor != null)
        {
            if (cursor.matches(name))
            {
                if (!force)
                    return;
                if (value != null)
                {
                    cursor.setValue(value);
                    return;
                }
                // Remove this Attribute node from the linked list
                if (prior == null)
                    firstAttribute = cursor.getNextAttribute();
                else
                    prior.setNextAttribute(cursor.getNextAttribute());
                return;
            }
            prior = cursor;
            cursor = cursor.getNextAttribute();
        }
        // Don't add a Attribute if the value is null.
        if (value == null)
            return;
        firstAttribute = new Attribute(name, value, firstAttribute);
    }

    /**
     * Convenience for invoking {@link #attribute(String, String)} multiple times.
     *
     * @param namesAndValues alternating attribute names and attribute values
     */
    public Element attributes(String... namesAndValues)
    {
        int i = 0;
        while (i < namesAndValues.length)
        {
            String name = namesAndValues[i++];
            String value = namesAndValues[i++];
            attribute(name, value);
        }
        return this;
    }

    /**
     * Forces changes to a number of attributes. The new attributes <em>overwrite</em> previous values. Overriding an
     * attribute's value to null will remove the attribute entirely.
     *
     * @param namesAndValues alternating attribute names and attribute values
     * @return this element
     */
    public Element forceAttributes(String... namesAndValues)
    {
        int i = 0;
        while (i < namesAndValues.length)
        {
            String name = namesAndValues[i++];
            String value = namesAndValues[i++];
            updateAttribute(name, value, true);
        }
        return this;
    }

    //    /**
    //     * Creates and returns a new Element node as a child of this node.
    //     *
    //     * @param name           the name of the element to create
    //     * @param namesAndValues alternating attribute names and attribute values
    //     */
    //    public Element element(String name, String... namesAndValues)
    //    {
    //        assert !Utils.isEmpty(name);
    //        Element child = newChild(new Element(name));
    //
    //        child.attributes(namesAndValues);
    //
    //        return child;
    //    }
    /**
     * Adds the comment and returns this element for further construction.
     */
    public Element comment(String text)
    {
        newChild(new Comment(text));
        return this;
    }

    /**
     * Adds the raw text and returns this element for further construction.
     */
    public Element raw(String text)
    {
        newChild(new Raw(text));
        return this;
    }

    /**
     * Adds and returns a new text node (the text node is returned so that {@link Text#write(String)} or [@link
     * {@link Text#writef(String, Object[])} may be invoked .
     *
     * @param text initial text for the node
     * @return the new Text node
     */
    public Text text(String text)
    {
        return newChild(new Text(text));
    }

    private <T extends Node> T newChild(T child)
    {
        addChild(child);
        return child;
    }

    public String getAttribute(String attributeName)
    {
        for (Attribute attr = firstAttribute; attr != null; attr = attr.getNextAttribute())
        {
            if (attr.getName().equalsIgnoreCase(attributeName))
                return attr.getValue();
        }
        return null;
    }

    /**
     * Adds one or more CSS class names to the "class" attribute. No check for duplicates is made. Note that CSS class
     * names are case insensitive on the client.
     *
     * @param className one or more CSS class names
     * @return the element for further configuration
     */
    public Element addClassName(String... className)
    {
        String classes = getAttribute(CLASS_ATTRIBUTE);
        StringBuilder builder = new StringBuilder();
        if (classes != null)
            builder.append(classes);
        for (String name : className)
        {
            if (builder.length() > 0)
                builder.append(" ");
            builder.append(name);
        }
        forceAttributes(CLASS_ATTRIBUTE, builder.toString());
        return this;
    }

    /**
     * Removes all children from this element.
     *
     * @return the element, for method chaining
     */
    public Element removeChildren()
    {
        firstChild = null;
        lastChild = null;
        return this;
    }

    /**
     * Returns true if the element has no children, or has only text children that contain only whitespace.
     *
     * @since 5.1.0.0
     */
    public boolean isEmpty()
    {
        List<INode> children = getChildren();
        if (children.isEmpty())
            return true;
        for (INode n : children)
        {
            if (n instanceof Text)
            {
                Text t = (Text)n;
                if (t.isEmpty())
                    continue;
            }
            // Not a text node, or a non-empty text node, then the element isn't empty.
            return false;
        }
        return true;
    }

    public void addChild(INode child)
    {
        child.setContainer(this);
        if (lastChild == null)
        {
            firstChild = child;
            lastChild = child;
            return;
        }
        lastChild.setNextSibling(child);
        lastChild = child;
    }

    void insertChildAt(int index, Node newChild)
    {
        newChild.setContainer(this);
        if (index < 1)
        {
            newChild.setNextSibling(firstChild);
            firstChild = newChild;
        }
        else
        {
            INode cursor = firstChild;
            for (int i = 1; i < index; i++)
            {
                cursor = cursor.getNextSibling();
            }
            newChild.setNextSibling(cursor.getNextSibling());
            cursor.setNextSibling(newChild);
        }
        if (index < 1)
        {
            firstChild = newChild;
        }
        if (newChild.getNextSibling() == null)
        {
            lastChild.setNextSibling(newChild);
        }
    }

    protected boolean hasChildren()
    {
        return firstChild != null;
    }

    protected void writeChildMarkup(RenderContext markupContext, PrintWriter writer)
    {
        INode cursor = firstChild;
        while (cursor != null)
        {
            cursor.toMarkup(markupContext, writer);
            cursor = cursor.getNextSibling();
        }
    }

    /**
     * Returns an unmodifiable list of children for this element. Only {@link org.apache.tapestry5.dom.Element}s will
     * have children. Also, note that unlike W3C DOM, attributes are not represented as
     * {@link org.apache.tapestry5.dom.Node}s.
     *
     * @return unmodifiable list of children nodes
     */
    @SuppressWarnings("unchecked")
    public List<INode> getChildren()
    {
        List<INode> result = new ArrayList<INode>();
        INode cursor = firstChild;
        while (cursor != null)
        {
            result.add(cursor);
            cursor = cursor.getNextSibling();
        }
        return result;
    }

    void remove(Node node)
    {
        INode prior = null;
        INode cursor = firstChild;
        while (cursor != null)
        {
            if (cursor == node)
            {
                INode afterNode = node.getNextSibling();
                if (prior != null)
                {
                    prior.setNextSibling(afterNode);
                }
                else
                {
                    firstChild = afterNode;
                }
                // If node was the final node in the element then handle deletion.
                // It's even possible node was the only node in the container.
                if (lastChild == node)
                {
                    lastChild = prior != null ? prior : null;
                }
                node.setNextSibling(null);
                return;
            }
            prior = cursor;
            cursor = cursor.getNextSibling();
        }
        throw new IllegalArgumentException(
            "Node to remove was not present as a child of this element.");
    }

    void insertChildBefore(Node existing, Node node)
    {
        int index = indexOfNode(existing);
        node.setContainer(this);
        insertChildAt(index, node);
    }

    void insertChildAfter(INode existing, INode node)
    {
        INode oldAfter = existing.getNextSibling();
        existing.setNextSibling(node);
        node.setNextSibling(oldAfter);
        if (oldAfter == null)
        {
            lastChild = node;
        }
        node.setContainer(this);
    }

    int indexOfNode(INode node)
    {
        int index = 0;
        INode cursor = firstChild;
        while (cursor != null)
        {
            if (node == cursor)
            {
                return index;
            }
            cursor = cursor.getNextSibling();
            index++;
        }
        throw new IllegalArgumentException("Node not a child of this element.");
    }

    /**
     * Returns the attributes for this Element as a (often empty) collection of
     * {@link org.apache.tapestry5.dom.Attribute}s. The order of the attributes within the collection is not specified.
     * Modifying the collection will not affect the attributes (use {@link #forceAttributes(String[])} to change
     * existing attribute values, and {@link #attribute(String, String, String)} to add new attribute values.
     *
     * @return attribute collection
     */
    public Collection<Attribute> getAttributes()
    {
        Collection<Attribute> result = new ArrayList<Attribute>();
        for (Attribute a = firstAttribute; a != null; a = a.getNextAttribute())
        {
            result.add(a);
        }
        return result;
    }

    protected Attribute getFirstAttribute()
    {
        return firstAttribute;
    }
}
